Hyunjung Im
Frontend Developer
2023-08-02
함수 이름에 있는 암묵적 인자 냄새는 두 가지 특징을 보인다.
암묵적 인자를 드러내기 리팩터링은 암묵적 인자가 일급 값이 되도록 함수에 인자를 추가한다. 이렇게 하면 잠재적 중복을 없애고 코드의 목적을 더 잘 표현할 수 있다.
기본적인 아이디어는 암묵적 인자를 명시적인 인자로 바꾸는 것이다.
단계
// 리팩터링 전
function setPriceByName(cart, name, price) {
// 함수 이름에 있는 price가 암묵적 인자이다.
const itme = cart[name];
const newItem = objectSet(item, "price", price);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.4);
// 리팩터링 후
function setFieldByName(cart, name, field, value) {
// 명시적인 인자를 추가한다.
// 원래 인자는 더 일반적인 이름으로 바꾼다.
const item = cart[name];
const newItem = objectSet(item, field, value);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
cart = setFieldByName(cart, "shoe", "price", 13);
cart = setFieldByName(cart, "shoe", "quantity", 3);
cart = setFieldByName(cart, "shoe", "shipping", 0);
cart = setFieldByName(cart, "shoe", "tax", 2.4);
다른 언어를 사용해도 그렇고 JavaScript에는 일급이 아닌 것과 일급인 것이 섞여 있다.
const validItemFields = ["price", "quantity", "shipping", "tax"];
function setFieldByName(cart, name, field, value) {
if (!validItemFields.includes(field)) throw "Not a valid item field: " + "'" + field + "'.";
}
function incrementQuantityByName(cart, name) {
const item = cart[name];
const quantity = item["quantity"];
const newQuantity = quantity + 1;
const newItem = objectSet(item, "quantity", newQuantity);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
function incrementSizeByName(cart, name) {
const item = cart[name];
const size = item["size"];
const newSize = size + 1;
const newItem = objectSet(item, "size", newSize);
const newCart = objectSet(cart, name, newItem);
return newCart;
}
function incrementFieldByName(cart, name, field) {
const item = cart[name];
const value = item[field];
const newValue = value + 1;
const newCart = objectSet(cart, name, newItem);
return newCart;
}
데이터 지향(data orientation)은 이벤트와 엔티티에 대한 사실을 표현하기 위해 일반 데이터 구조를 사용하는 프로그래밍 형식이다. 위의 장바구니와 제품 엔티티는 매우 일반적이다. 장바구니와 제품 엔티티는 커스텀 API처럼 구체적인 것보다는 낮은 곳에 위치한다. 그래서 장바구니와 제품 엔티티에 일반적인 데이터 구조인 객체와 배열을 사용하는 것이다.
많은 동적 타입 언어가 데이터 구조에 있는 필드를 문자열로 표현하고 전송한다. 오타나 잘못된 문자열로 인한 에러가 종종 발생하는 것도 맞다.
function cookAndEatFoods() {
for (let i = 0; i < foods.length; i++) {
const food = foods[i];
cook(food);
eat(food);
}
}
function cleanDishes() {
for (let i = 0; i < dishes.length; i++) {
const dish = dishes[i];
wash(dish);
dry(dish);
putAway(dish);
}
}
function operateOnArray(array, f) {
for (let i = 0; i < array.length; i++) {
const item = array[i];
f(item);
}
}
operateOnArray(foods, cookAndEat);
operateOnArray(foods, clean);
forEach()
고차 함수를 이용할 수 있다.45000줄의 코드를 모두 try/catch로 감싸 에러 로깅 시스템을 적용해야 한다고 생각해보자. 중복된 코드가 굉장히 많을 것이다.
// 원래 코드
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
// 함수로 빼낸 코드
function withLogging() {
try {
saveUserData(user);
} catch (error) {
logToSnapErrors(error);
}
}
// 콜백으로 빼낸 코드
function withLogging(f) {
try {
f();
} catch (error) {
saveUserData(user);
}
}
withLogging(function () {
saveUserData(user);
}); // 본문을 전달한다.
위 코드의 saveUserData()
함수에서 에러가 나면 어떻게 될까? withLogging()
함수에 있는 try/catch
가 처리해줄까?
try/catch
라고 할 수 있다.// 리팩터링 전
function arraySet(array, idx, value) {
const copy = array.slice();
copy[idx] = value;
return copy;
}
// 리팩터링 후
function arraySet(array, idx, value) {
return withArrayCopy(array, function (copy) {
copy[idx] = value;
});
}
function withArrayCopy(array, modify) {
const copy = array.slice();
modify(copy);
return copy;
}
withArrayCopy()
함수를 쓰면 최적화를 위해 복사본을 하나만 만들어 쓸 수 있다.map()
, filter()
, reduce()
는 함수형 도구의 전부가 아니다. 자주 사용하는 함수형 도구일 뿐이다.// 절차형
function shoesAndSocksInventory(products) {
var inventory = 0;
for (let p = 0; p < products.length; p++) {
const product = products[p];
if (product.type === "shoes" || product.type === "socks") {
inventory += product.numberInventory;
}
}
return inventory;
}
// 함수형으로 개선
function shoesAndSocksInventory(products) {
const shoesAndSocks = filter(
products,
(product) => product.type === "shoes" || product.type === "socks"
);
const inventories = map(shoesAndSocks, (product) => product.numberInventory);
return reduce(inventories, 0, plus);
}
고차 함수를 사용하는 것은 매우 추상적이기 때문에 문제가 생겼을 때 이해하기 어려운 때도 있다.
map()
, filter()
그리고 reduce()
가 가장 단순하고 많이 쓰는 도구이다. 함수형 도구 문서를 살펴보면 영감을 얻는 데 도움이 된다.
pluck()
map()
으로 특정 필드값을 가져오기 위해 콜백을 매번 작성하는 것은 번거롭다. pluck()
을 사용하면 매번 작성하지 않아도 된다.
function pluck(array, field) {
return map(array, function (object) {
return object[field];
});
}
// 사용법
const prices = pluck(products, "price");
// 비슷한 도구
function invokeMap(array, method) {
return map(array, function (object) {
return object[method]();
});
}
concat()
concat()
으로 배열 안에 배열을 뺄 수 있다. 중첩된 배열을 한 단계의 배열로 만든다.
frequenciesBy()와 groupBy()
개수를 세거나 그룹화하는 일은 종종 쓸모가 있다. 이 함수는 객체 또는 맵을 리턴한다.
function frequenciesBy(array, f) {
const ret = {};
forEach(array, function (element) {
const key = f(element);
if (ret[key]) ret[key] += 1;
else ret[key] = 1;
});
return ret;
}
function groupBy(array, f) {
const ret = {};
forEach(array, function (element) {
const key = f(element);
if (ret[key]) ret[key].push(element);
else ret[key] = [element];
});
return ret;
}
전형적으로 함수형 프로그래밍의 자격 요건에는 관계형 정의뿐 아니라 타입, 패턴 매치, 불변성, 순수성 같은 명확히 다른 자격 요건들도 들어간다. 각각의 특성이 함수형 프로그래밍 언어의 특정 부분을 설명할 수는 있지만 포괄적인 함수형 프로그래밍 언어를 대변할 수는 없다.
프로그램을 여러 구성 요소로 분해하고, 추상화된 함수를 이용해서 본래의 기능을 수행하도록 재조립하는 것이 함수형 프로그래밍이다.
코드가 어떤 동작을 하도록 구현하는 것을 프로그래밍이라고 한다면 어떤 것이 해석되는 방식을 바꾸도록 코드를 구현하는 것을 메타프로그래밍이라고 한다.
function doubleField(item, field) {
const value = item[field];
const newValue = value + 1;
const newItem = objectSet(item, field, newValue);
return newItem;
}
function decrementField(item, field) {...}
function doubleField(item, field) {...}
function halveField(item, field) {...} // 본문들 다 비슷한 함수를 가지고 있다.
함수 이름에 있는 암묵적 인자
냄새와 비슷하다. 각 함수 이름에는 동작이름이 있다.function update(item, field, modify) {
const value = item[field];
const newValue = modify(value);
const newItem = objectSet(item, field, newValue);
return newItem;
}
function incrementField(item, field) {
return updateField(item, field, (value) => value + 1);
}
objectSet()
을 사용하기 때문에 카피-온-라이트 원칙을 따른다.update
함수를 여러번 사용해야 한다면?function updateX(object, keys, modify) {
if (keys.length === 0) {
return modify(object);
}
const key1 = keys[0];
const restOfKeys = drop_first(keys);
return update(object, key1, function (value) {
return updateX(value1, restOfKeys, modify); // 재귀 호출
});
}
updateX
보다 nestedUpdate
라고 사용하는 것이 더 일반적일 수 있다.주어진 ID로 블로그를 변경하는 함수가 있다고 생각해보자.
function updatePostById(category, id, modifyPost) {
return nestedUpdate(category, ["posts", id], modifyPost);
}
["posts", id]
로 분류의 구조 같은 구체적인 부분은 추상화 벽 뒤로 숨긴다.function updateAuthor(post, modifyUser) {
return update(post, "author", modifyUser);
}
function capitalizeName(user) {
return update(user, "name", capitalize);
}
updatePostById(blogCategory, "12", (post) => updateAuthor(post, capitalizeUserName));
타임라인은 액션을 순서대로 나열한 것이다. 타임라인 다이어그램은 시간에 따른 액션 순서를 시각적으로 표시한 것이다.
function add_item_to_cart(name, price, quantity) {
cart = add_item(cart, name, price, quantity);
calc_cart_total();
}
// cart 읽기
// cart 쓰기
function calc_cart_total() {
total = 0; // total = 0 쓰기
cost_ajax(cart, function (cast) {
// cart 읽기, cost_ajax() 부르기
total += cost; // total 읽기, 쓰기
shipping_ajax(cart, function (shipping) {
// cart 읽기, shipping_ajax() 부르기
total += shipping; // total 읽기, 쓰기
update_total_dom(total); // total 읽기, update_total_dom() 부르기
});
});
}
saveUserAjax(user, function () {
// 서버에 사용자 저장
setUserLoadingDOM(false); // 사용자 로딩 표시 감추기
});
setUserLoadingDOM(true); // 사용자 로딩 표시 하기
saveDocumentAjax(document, function () {
// 서버에 문서 저장
setDocLoadingDOM(false); // 문서 로딩 표시 감추기
});
setDocLoadingDOM(true); // 문서 로딩 표시 하기